In [1]:
#01 Import, install and create path
In [2]:
#01.1 Install 3D visualizer
!pip install plotly==5.12.0
Requirement already satisfied: plotly==5.12.0 in c:\users\jonba\anaconda3\lib\site-packages (5.12.0)
Requirement already satisfied: tenacity>=6.2.0 in c:\users\jonba\anaconda3\lib\site-packages (from plotly==5.12.0) (8.2.2)
In [3]:
#01.2 Import libraries
import pandas as pd
import plotly.express as px
import numpy as np
import matplotlib.pyplot as plt
import os
from mpl_toolkits import mplot3d

from plotly.offline import download_plotlyjs, init_notebook_mode
from plotly.offline import plot, iplot
import plotly.graph_objects as go
In [4]:
#01.3 Create a path to where your data is stored
path = r'D:\Joan\CareerFoundry\Machine Learning\Datasets\Raw Data'
In [5]:
#01.4 Read in the European weather data
climate = pd.read_csv(os.path.join(path, 'Dataset-weather-prediction-dataset-processed.csv'))
climate
Out[5]:
DATE MONTH BASEL_cloud_cover BASEL_wind_speed BASEL_humidity BASEL_pressure BASEL_global_radiation BASEL_precipitation BASEL_snow_depth BASEL_sunshine ... VALENTIA_cloud_cover VALENTIA_humidity VALENTIA_pressure VALENTIA_global_radiation VALENTIA_precipitation VALENTIA_snow_depth VALENTIA_sunshine VALENTIA_temp_mean VALENTIA_temp_min VALENTIA_temp_max
0 19600101 1 7 2.1 0.85 1.0180 0.32 0.09 0 0.7 ... 5 0.88 1.0003 0.45 0.34 0 4.7 8.5 6.0 10.9
1 19600102 1 6 2.1 0.84 1.0180 0.36 1.05 0 1.1 ... 7 0.91 1.0007 0.25 0.84 0 0.7 8.9 5.6 12.1
2 19600103 1 8 2.1 0.90 1.0180 0.18 0.30 0 0.0 ... 7 0.91 1.0096 0.17 0.08 0 0.1 10.5 8.1 12.9
3 19600104 1 3 2.1 0.92 1.0180 0.58 0.00 0 4.1 ... 7 0.86 1.0184 0.13 0.98 0 0.0 7.4 7.3 10.6
4 19600105 1 6 2.1 0.95 1.0180 0.65 0.14 0 5.4 ... 3 0.80 1.0328 0.46 0.00 0 5.7 5.7 3.0 8.4
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
22945 20221027 10 1 2.1 0.79 1.0248 1.34 0.22 0 7.7 ... 5 0.82 1.0142 1.13 0.41 0 3.4 10.7 7.9 13.5
22946 20221028 10 6 2.1 0.77 1.0244 1.34 0.22 0 5.4 ... 5 0.82 1.0142 1.13 0.41 0 3.4 10.7 7.9 13.5
22947 20221029 10 4 2.1 0.76 1.0227 1.34 0.22 0 6.1 ... 5 0.82 1.0142 1.13 0.41 0 3.4 10.7 7.9 13.5
22948 20221030 10 5 2.1 0.80 1.0212 1.34 0.22 0 5.8 ... 5 0.82 1.0142 1.13 0.41 0 3.4 10.7 7.9 13.5
22949 20221031 10 5 2.1 0.84 1.0193 1.34 0.22 0 3.2 ... 5 0.82 1.0142 1.13 0.41 0 3.4 10.7 7.9 13.5

22950 rows × 170 columns

In [6]:
#02 Adjust df and create subsets
In [7]:
#02.1 Reduce to just the mean temperatures
df = climate[['DATE', 'MONTH','BASEL_temp_mean',
    'BELGRADE_temp_mean',
    'BUDAPEST_temp_mean',
    'DEBILT_temp_mean',
    'DUSSELDORF_temp_mean',
    'GDANSK_temp_mean',
    'HEATHROW_temp_mean',
    'KASSEL_temp_mean',
    'LJUBLJANA_temp_mean',
    'MAASTRICHT_temp_mean',
    'MADRID_temp_mean',
    'MUNCHENB_temp_mean',
    'OSLO_temp_mean',
    'ROMA_temp_mean',
    'SONNBLICK_temp_mean',
    'STOCKHOLM_temp_mean',
    'TOURS_temp_mean',
    'VALENTIA_temp_mean']].copy()
In [8]:
#02.2 show dataframe
df
Out[8]:
DATE MONTH BASEL_temp_mean BELGRADE_temp_mean BUDAPEST_temp_mean DEBILT_temp_mean DUSSELDORF_temp_mean GDANSK_temp_mean HEATHROW_temp_mean KASSEL_temp_mean LJUBLJANA_temp_mean MAASTRICHT_temp_mean MADRID_temp_mean MUNCHENB_temp_mean OSLO_temp_mean ROMA_temp_mean SONNBLICK_temp_mean STOCKHOLM_temp_mean TOURS_temp_mean VALENTIA_temp_mean
0 19600101 1 6.5 3.7 2.4 9.3 10.0 0.8 10.6 7.9 -0.6 9.5 7.6 6.9 4.9 7.8 -5.9 4.2 10.0 8.5
1 19600102 1 6.1 2.9 2.3 7.7 8.2 1.6 6.1 7.7 2.1 8.6 9.8 6.2 3.4 12.2 -9.5 4.0 9.5 8.9
2 19600103 1 8.5 3.1 2.7 6.8 7.1 0.7 8.4 6.5 4.6 6.9 8.6 5.8 1.9 10.2 -9.5 2.4 10.3 10.5
3 19600104 1 6.3 2.0 2.0 6.7 6.8 -0.1 9.4 5.8 3.2 7.0 10.3 3.9 3.0 10.8 -11.5 1.2 11.2 7.4
4 19600105 1 3.0 2.0 2.5 8.0 7.7 0.4 8.9 5.4 3.6 8.1 12.1 1.8 3.7 9.9 -9.3 3.3 11.4 5.7
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
22945 20221027 10 15.9 18.2 11.7 15.7 17.8 11.5 16.4 9.1 14.7 18.6 20.0 14.3 9.7 15.4 0.6 11.5 19.9 10.7
22946 20221028 10 16.7 15.9 11.7 16.0 19.4 11.7 15.8 9.1 12.9 18.9 19.1 16.1 10.9 15.4 2.3 12.5 20.3 10.7
22947 20221029 10 16.7 13.4 11.7 15.8 18.2 14.2 16.5 9.1 13.2 18.2 19.0 17.4 9.7 15.4 3.3 13.1 20.6 10.7
22948 20221030 10 15.4 15.0 11.7 14.4 16.7 11.0 15.2 9.1 14.0 16.3 15.7 14.5 5.9 15.4 3.4 7.5 15.9 10.7
22949 20221031 10 13.5 14.4 11.7 12.8 15.2 9.3 13.7 9.1 13.6 15.3 14.1 12.9 9.2 15.4 1.7 9.7 16.8 10.7

22950 rows × 20 columns

In [9]:
#02.3 Drop DATE and MONTH columns
notemp = df.drop(['DATE','MONTH'], axis=1)
In [10]:
#02.4 create whisker plot to see variations in temperature
notemp.boxplot(figsize=(15,15))
plt.xticks(rotation=90)
Out[10]:
(array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17,
        18]),
 [Text(1, 0, 'BASEL_temp_mean'),
  Text(2, 0, 'BELGRADE_temp_mean'),
  Text(3, 0, 'BUDAPEST_temp_mean'),
  Text(4, 0, 'DEBILT_temp_mean'),
  Text(5, 0, 'DUSSELDORF_temp_mean'),
  Text(6, 0, 'GDANSK_temp_mean'),
  Text(7, 0, 'HEATHROW_temp_mean'),
  Text(8, 0, 'KASSEL_temp_mean'),
  Text(9, 0, 'LJUBLJANA_temp_mean'),
  Text(10, 0, 'MAASTRICHT_temp_mean'),
  Text(11, 0, 'MADRID_temp_mean'),
  Text(12, 0, 'MUNCHENB_temp_mean'),
  Text(13, 0, 'OSLO_temp_mean'),
  Text(14, 0, 'ROMA_temp_mean'),
  Text(15, 0, 'SONNBLICK_temp_mean'),
  Text(16, 0, 'STOCKHOLM_temp_mean'),
  Text(17, 0, 'TOURS_temp_mean'),
  Text(18, 0, 'VALENTIA_temp_mean')])
No description has been provided for this image
In [11]:
#02.5 Reduce to single year
dfyear = df[df['DATE'].astype(str).str.contains('2000')]
dfyear
Out[11]:
DATE MONTH BASEL_temp_mean BELGRADE_temp_mean BUDAPEST_temp_mean DEBILT_temp_mean DUSSELDORF_temp_mean GDANSK_temp_mean HEATHROW_temp_mean KASSEL_temp_mean LJUBLJANA_temp_mean MAASTRICHT_temp_mean MADRID_temp_mean MUNCHENB_temp_mean OSLO_temp_mean ROMA_temp_mean SONNBLICK_temp_mean STOCKHOLM_temp_mean TOURS_temp_mean VALENTIA_temp_mean
14610 20000101 1 2.9 -2.5 -4.9 6.1 4.2 -0.7 7.0 3.5 -4.8 5.6 5.4 1.7 -5.0 15.4 -15.2 -2.3 8.5 6.6
14611 20000102 1 3.6 -1.2 -3.6 7.3 6.5 -0.3 7.9 2.3 -0.9 6.2 5.0 1.9 -0.8 4.2 -13.7 1.3 7.9 9.6
14612 20000103 1 2.2 -1.0 -0.8 8.4 7.7 3.2 9.4 3.5 -0.3 6.8 3.5 -0.4 1.2 3.8 -9.2 0.8 8.1 8.6
14613 20000104 1 3.9 -1.3 -1.0 6.4 7.8 3.7 7.0 4.8 -3.6 7.3 4.3 3.8 2.1 6.0 -5.6 3.5 8.6 8.1
14614 20000105 1 6.0 -0.8 0.2 4.4 5.2 1.9 6.4 2.3 -3.0 5.2 0.6 5.3 -0.7 5.0 -7.6 -0.6 8.0 7.7
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
14971 20001227 12 5.2 12.3 4.4 -1.0 1.4 -1.7 2.2 1.4 6.4 -0.1 6.8 0.8 -3.2 13.1 -10.3 -0.3 6.7 1.4
14972 20001228 12 2.6 11.7 6.2 0.5 1.1 -0.7 -0.3 1.0 6.4 0.8 5.4 1.6 -3.5 11.3 -10.4 0.7 2.4 2.0
14973 20001229 12 1.1 7.4 4.6 0.6 0.3 -1.1 -2.2 -1.5 3.0 0.2 8.6 -0.7 -4.3 10.5 -10.5 0.3 3.3 2.4
14974 20001230 12 1.0 4.3 2.5 1.7 1.3 -3.0 -1.1 0.2 1.6 0.6 7.0 -0.4 -7.3 7.5 -13.9 1.2 1.2 3.7
14975 20001231 12 0.6 2.0 1.3 -0.1 1.2 1.0 2.8 -0.8 -0.5 1.2 4.8 -2.7 -13.3 9.5 -16.7 -0.8 1.9 4.8

366 rows × 20 columns

In [12]:
#02.6 Describe dataframe
dfyear.describe()
Out[12]:
DATE MONTH BASEL_temp_mean BELGRADE_temp_mean BUDAPEST_temp_mean DEBILT_temp_mean DUSSELDORF_temp_mean GDANSK_temp_mean HEATHROW_temp_mean KASSEL_temp_mean LJUBLJANA_temp_mean MAASTRICHT_temp_mean MADRID_temp_mean MUNCHENB_temp_mean OSLO_temp_mean ROMA_temp_mean SONNBLICK_temp_mean STOCKHOLM_temp_mean TOURS_temp_mean VALENTIA_temp_mean
count 3.660000e+02 366.000000 366.000000 366.000000 366.000000 366.000000 366.000000 366.000000 366.000000 366.000000 366.000000 366.000000 366.000000 366.000000 366.000000 366.000000 366.000000 366.000000 366.000000 366.000000
mean 2.000067e+07 6.513661 11.786612 14.195902 12.694262 10.896995 11.449454 9.173497 11.582514 10.081967 12.181148 11.123497 15.006557 10.592896 7.801639 16.014754 -4.286885 8.497814 12.313115 10.758197
std 3.457653e+02 3.455958 6.455730 8.963638 8.408440 5.450340 5.852145 6.340916 5.155321 6.358591 7.858359 5.788827 7.130383 7.224441 6.646408 6.372385 6.422520 6.546361 5.611616 3.722074
min 2.000010e+07 1.000000 -6.800000 -9.900000 -6.600000 -2.800000 -2.600000 -10.400000 -2.200000 -6.500000 -7.900000 -3.400000 0.600000 -12.200000 -13.300000 1.700000 -23.500000 -10.100000 -2.000000 1.400000
25% 2.000040e+07 4.000000 6.825000 7.400000 5.950000 6.600000 6.925000 4.400000 7.600000 5.400000 6.325000 6.800000 9.400000 5.225000 2.900000 10.400000 -8.750000 3.025000 8.200000 7.900000
50% 2.000070e+07 7.000000 11.800000 15.300000 13.750000 10.800000 11.450000 9.900000 11.400000 10.050000 13.150000 11.050000 13.900000 10.800000 8.550000 16.050000 -3.700000 9.300000 11.600000 10.500000
75% 2.000098e+07 9.750000 17.100000 20.700000 19.300000 15.200000 15.775000 14.475000 15.875000 15.375000 18.300000 15.675000 21.400000 16.375000 13.175000 21.400000 0.575000 13.900000 17.175000 13.900000
max 2.000123e+07 12.000000 25.100000 32.800000 29.500000 25.900000 28.600000 22.200000 25.400000 25.400000 26.900000 26.900000 29.400000 26.800000 19.900000 29.500000 8.500000 21.700000 26.200000 19.500000

03 Visualization for one weather station¶

In [14]:
#03.1 Drop date and month
notempyear = dfyear.drop(['DATE','MONTH'], axis=1)
In [15]:
#03.2 Plot all weather data for all stations for a year
#X = weather station
#Y = day of the year
#Z = temperature

fig = go.Figure(data=[go.Surface(z=notempyear.values)])
fig.update_layout(title='Temperatures over time', autosize=False,
                width=600, height=600)
fig.show()
In [16]:
#We need to make an index for the year. Create a set of data from 1 to 365 (or to 366 if it's a leap year!)
#We'll scale this by 100 as the index is made. This will help the gradient descent converge 366 = 3.66

i = np.arange(0.01,3.67,0.01)
index = pd.DataFrame(data = i, columns = ['index'])
index
Out[16]:
index
0 0.01
1 0.02
2 0.03
3 0.04
4 0.05
... ...
361 3.62
362 3.63
363 3.64
364 3.65
365 3.66

366 rows × 1 columns

In [17]:
#03.4 Count of rows
n_rows = dfyear.shape[0]
n_rows
Out[17]:
366
In [18]:
#This will translate your chosen weather data into the X and y datasets needed for the optimization function.

X=index.to_numpy().reshape(n_rows,1)
#Represent x_0 as a vector of 1s for vector computation
ones = np.ones((n_rows,1))
X = np.concatenate((ones, X), axis=1)
y=dfyear['MADRID_temp_mean'].to_numpy().reshape(n_rows,1) #<----INSERT WEATHER STATION HERE"
In [19]:
#03.6 show shape of data
X.shape, y.shape
Out[19]:
((366, 2), (366, 1))
In [20]:
#03.7 One year of temperature data from 2000 Heathrow
plt.scatter(x=index['index'], y=dfyear['MADRID_temp_mean'])
plt.xlabel('X'); plt.ylabel('y');
plt.title('Input dataset');
In [21]:
#03.8 Find min temp
dfyear['MADRID_temp_mean'].min()
Out[21]:
0.6
In [22]:
#03.9 Find max temp
dfyear['MADRID_temp_mean'].max()
Out[22]:
29.4

04 Gradient Descent¶

In [24]:
#04.1 Compute loss function This computes the loss function for the gradiant descent. DO NOT CHANGE!

def compute_cost(X, y, theta=np.array([[0],[0]])):
    """Given covariate matrix X, the prediction results y and coefficients theta
    compute the loss"""

    m = len(y)
    J=0 # initialize loss to zero

    # reshape theta
    theta=theta.reshape(2,1)

    # calculate the hypothesis - y_hat
    h_x = np.dot(X,theta)
    #print(h_x)

     # subtract y from y_hat, square and sum
    error_term = sum((h_x - y)**2)

    # divide by twice the number of samples - standard practice.
    loss = error_term/(2*m)

    return loss
In [25]:
#04.2 compute cost
compute_cost(X,y)
Out[25]:
array([137.95010929])
In [26]:
#04.3 Gradiant Descent Function
def gradient_descent(X, y, theta=np.array([[0],[0]]),
                    alpha=0.01, num_iterations=1500):

    """Solve for theta using Gradient Descent optimiztion technique. Alpha is the learning rate"""

    m = len(y)
    J_history = []
    theta0_history = []
    theta1_history = []
    theta = theta.reshape(2,1)

    for i in range(num_iterations):
        error = (np.dot(X, theta) - y)

        term0 = (alpha/m) * sum(error* X[:,0].reshape(m,1))
        term1 = (alpha/m) * sum(error* X[:,1].reshape(m,1))

        # update theta
        term_vector = np.array([[term0],[term1]])
        #print(term_vector)
        theta = theta - term_vector.reshape(2,1)

        # store history values
        theta0_history.append(theta[0].tolist()[0])
        theta1_history.append(theta[1].tolist()[0])
        J_history.append(compute_cost(X,y,theta).tolist()[0])

    return (theta, J_history, theta0_history, theta1_history)
In [27]:
#04.4 Run data through gradient descent
num_iterations=250 #<---Decide how many iterations you need. Start small and work up. Over 10,000 iterations will take a few seconds.
theta_init=np.array([[0],[0]]) #<---this is where you put the guess for [theta0], [theta1]. Start with 1 and 1.
alpha=0.1 #<---Decide what your step size is. Try values between 0.1 and 0.00001. You will need to adjust your iterations.
#If your solution is not converging, try a smaller step size.
theta, J_history, theta0_history, theta1_history = gradient_descent(X,y, theta_init, 
                                                                    alpha, num_iterations)
In [28]:
#04.5 show theta
theta
Out[28]:
array([[12.80532739],
       [ 1.19387058]])
In [29]:
#04.6 Plot loss, theta0 and theta1. Loss should trend toward 0.
fig, ax1 = plt.subplots()

# plot thetas over time
color='tab:blue'
ax1.plot(theta0_history, label='theta_{0}', linestyle='--', color=color)
ax1.plot(theta1_history, label='theta_{1}', linestyle='-', color=color)
# ax1.legend()
ax1.set_xlabel('Iterations'); ax1.set_ylabel('theta', color=color);
ax1.tick_params(axis='y', labelcolor=color)

# plot loss function over time
color='tab:red'
ax2 = ax1.twinx()
ax2.plot(J_history, label='Loss function', color=color)
ax2.set_title('Values of theta and J(theta) over iterations')
ax2.set_ylabel('Loss: J(theta)', color=color)
ax1.tick_params(axis='y', labelcolor=color)

# ax2.legend();
fig.legend();
plt.show()
No description has been provided for this image
No description has been provided for this image
In [30]:
%%time
# theta range
theta0_vals = np.linspace(-10,10,100) #Look in the chart above for the limits of where theta0 and theta1 appear.
theta1_vals = np.linspace(-10,10,100) #Put those values as the first two \"linspace\" numbers in these lines
                                        #Select with large margins, maybe +/- 10
J_vals = np.zeros((len(theta0_vals), len(theta1_vals)))

# compute cost for each combination of theta
c1=0; c2=0
for i in theta0_vals:
    for j in theta1_vals:
        t = np.array([i, j])
        J_vals[c1][c2] = compute_cost(X, y, t.transpose()).tolist()[0]
        c2=c2+1
    c1=c1+1
    c2=0 # reinitialize to 0"
CPU times: total: 2.22 s
Wall time: 2.21 s
In [31]:
#This figure shows the loss function.

#X = Theta0
#Y - Theta1
#Z = Loss
#Find where it is closest to 0 in X and Y!

#you can click/hold in the graph below to rotate!

fig = go.Figure(data=[go.Surface(x=theta1_vals, y=theta0_vals, z=J_vals)])
fig.update_layout(title='Loss function for different thetas', autosize=True,
                  width=600, height=600, xaxis_title='theta0',
                  yaxis_title='theta1')
fig.show()
In [32]:
#Here is the same figure as above, with the line the loss function takes toward the minimum.

#X = Theta0
#Y - Theta1
#Z = Loss
#black line = path of loss function over the iterations.
#Find where it is closest to 0 in X and Y!

#you can click/hold in the graph below to rotate!

line_marker = dict(color='#101010', width=2)
fig = go.Figure()
fig.add_surface(x=theta1_vals, y=theta0_vals, z=J_vals)
fig.add_scatter3d(x=theta1_history, y=theta0_history, z=J_history, line=line_marker, name='')
#The below line adds a graph of just the loss over iterations in a 2D plane
plt.plot(theta0_history, theta1_history, 'r+');
fig.update_layout(title='Loss function for different thetas', autosize=True,
                  width=600, height=600, xaxis_title='theta0',
                  yaxis_title='theta1')
fig.show()
In [33]:
#Rerun the optimization above, but this time start closer to the objective!
#Find where the black line ends near the lowest X/Y/Z coordinate and make that your guess below.

num_iterations=10 #<---start with the same iterations as above
theta_init=np.array([[10],[10]]) #<---make a guess as to a more accurate [x],[y] coordinates near the minimum in the graph above.
alpha= 0.1 #<---start with the same step size as above
theta1, J_history1, theta0_history1, theta1_history1 = gradient_descent(X,y, theta_init,
                                                                        alpha, num_iterations)
In [34]:
#Let's look at the new loss path on the function. It should start much closer to the goal

line_marker = dict(color='#101010', width=2)
fig = go.Figure()
fig.add_surface(x=theta1_vals, y=theta0_vals, z=J_vals)
fig.add_scatter3d(x=theta1_history1, y=theta0_history1, z=J_history1, line=line_marker, name='')
#The below line adds a graph of just the loss over iterations in a 2D plane
plt.plot(theta0_history1, theta1_history1, 'r+');
fig.update_layout(title='Loss function for different thetas', autosize=True,
                  width=600, height=600, xaxis_title='theta0',
                  yaxis_title='theta1')
fig.show()
In [35]:
#This plot shows the convergence similar to above, but only in the X/Y plane (there's no height)

plt.contour(theta0_vals, theta1_vals, J_vals, levels = np.logspace(0,10,1000))
plt.xlabel('$\\\\theta_{0}$'); plt.ylabel('$\\\\theta_{1}$')
plt.title('Contour plot of loss function for different values of $\\\\theta$s');
plt.plot(theta0_history1, theta1_history1, 'r+');
plt.show()
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
File ~\anaconda3\Lib\site-packages\IPython\core\formatters.py:343, in BaseFormatter.__call__(self, obj)
    341     pass
    342 else:
--> 343     return printer(obj)
    344 # Finally look for special method names
    345 method = get_real_method(obj, self.print_method)

File ~\anaconda3\Lib\site-packages\IPython\core\pylabtools.py:170, in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
    167     from matplotlib.backend_bases import FigureCanvasBase
    168     FigureCanvasBase(fig)
--> 170 fig.canvas.print_figure(bytes_io, **kw)
    171 data = bytes_io.getvalue()
    172 if fmt == 'svg':

File ~\anaconda3\Lib\site-packages\matplotlib\backend_bases.py:2164, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2161     # we do this instead of `self.figure.draw_without_rendering`
   2162     # so that we can inject the orientation
   2163     with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2164         self.figure.draw(renderer)
   2165 if bbox_inches:
   2166     if bbox_inches == "tight":

File ~\anaconda3\Lib\site-packages\matplotlib\artist.py:95, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     93 @wraps(draw)
     94 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 95     result = draw(artist, renderer, *args, **kwargs)
     96     if renderer._rasterizing:
     97         renderer.stop_rasterizing()

File ~\anaconda3\Lib\site-packages\matplotlib\artist.py:72, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     69     if artist.get_agg_filter() is not None:
     70         renderer.start_filter()
---> 72     return draw(artist, renderer)
     73 finally:
     74     if artist.get_agg_filter() is not None:

File ~\anaconda3\Lib\site-packages\matplotlib\figure.py:3154, in Figure.draw(self, renderer)
   3151         # ValueError can occur when resizing a window.
   3153 self.patch.draw(renderer)
-> 3154 mimage._draw_list_compositing_images(
   3155     renderer, self, artists, self.suppressComposite)
   3157 for sfig in self.subfigs:
   3158     sfig.draw(renderer)

File ~\anaconda3\Lib\site-packages\matplotlib\image.py:132, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
    130 if not_composite or not has_images:
    131     for a in artists:
--> 132         a.draw(renderer)
    133 else:
    134     # Composite any adjacent images together
    135     image_group = []

File ~\anaconda3\Lib\site-packages\matplotlib\artist.py:72, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     69     if artist.get_agg_filter() is not None:
     70         renderer.start_filter()
---> 72     return draw(artist, renderer)
     73 finally:
     74     if artist.get_agg_filter() is not None:

File ~\anaconda3\Lib\site-packages\matplotlib\axes\_base.py:3034, in _AxesBase.draw(self, renderer)
   3031     for spine in self.spines.values():
   3032         artists.remove(spine)
-> 3034 self._update_title_position(renderer)
   3036 if not self.axison:
   3037     for _axis in self._axis_map.values():

File ~\anaconda3\Lib\site-packages\matplotlib\axes\_base.py:2978, in _AxesBase._update_title_position(self, renderer)
   2976 top = max(top, bb.ymax)
   2977 if title.get_text():
-> 2978     ax.yaxis.get_tightbbox(renderer)  # update offsetText
   2979     if ax.yaxis.offsetText.get_text():
   2980         bb = ax.yaxis.offsetText.get_tightbbox(renderer)

File ~\anaconda3\Lib\site-packages\matplotlib\axis.py:1352, in Axis.get_tightbbox(self, renderer, for_layout_only)
   1350 # take care of label
   1351 if self.label.get_visible():
-> 1352     bb = self.label.get_window_extent(renderer)
   1353     # for constrained/tight_layout, we want to ignore the label's
   1354     # width/height because the adjustments they make can't be improved.
   1355     # this code collapses the relevant direction
   1356     if for_layout_only:

File ~\anaconda3\Lib\site-packages\matplotlib\text.py:956, in Text.get_window_extent(self, renderer, dpi)
    951     raise RuntimeError(
    952         "Cannot get window extent of text w/o renderer. You likely "
    953         "want to call 'figure.draw_without_rendering()' first.")
    955 with cbook._setattr_cm(self.figure, dpi=dpi):
--> 956     bbox, info, descent = self._get_layout(self._renderer)
    957     x, y = self.get_unitless_position()
    958     x, y = self.get_transform().transform((x, y))

File ~\anaconda3\Lib\site-packages\matplotlib\text.py:381, in Text._get_layout(self, renderer)
    379 clean_line, ismath = self._preprocess_math(line)
    380 if clean_line:
--> 381     w, h, d = _get_text_metrics_with_cache(
    382         renderer, clean_line, self._fontproperties,
    383         ismath=ismath, dpi=self.figure.dpi)
    384 else:
    385     w = h = d = 0

File ~\anaconda3\Lib\site-packages\matplotlib\text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~\anaconda3\Lib\site-packages\matplotlib\text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~\anaconda3\Lib\site-packages\matplotlib\backends\backend_agg.py:217, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    213     return super().get_text_width_height_descent(s, prop, ismath)
    215 if ismath:
    216     ox, oy, width, height, descent, font_image = \
--> 217         self.mathtext_parser.parse(s, self.dpi, prop)
    218     return width, height, descent
    220 font = self._prepare_font(prop)

File ~\anaconda3\Lib\site-packages\matplotlib\mathtext.py:79, in MathTextParser.parse(self, s, dpi, prop, antialiased)
     77 prop = prop.copy() if prop is not None else None
     78 antialiased = mpl._val_or_rc(antialiased, 'text.antialiased')
---> 79 return self._parse_cached(s, dpi, prop, antialiased)

File ~\anaconda3\Lib\site-packages\matplotlib\mathtext.py:100, in MathTextParser._parse_cached(self, s, dpi, prop, antialiased)
     97 if self._parser is None:  # Cache the parser globally.
     98     self.__class__._parser = _mathtext.Parser()
--> 100 box = self._parser.parse(s, fontset, fontsize, dpi)
    101 output = _mathtext.ship(box)
    102 if self._output_type == "vector":

File ~\anaconda3\Lib\site-packages\matplotlib\_mathtext.py:2165, in Parser.parse(self, s, fonts_object, fontsize, dpi)
   2162     result = self._expression.parseString(s)
   2163 except ParseBaseException as err:
   2164     # explain becomes a plain method on pyparsing 3 (err.explain(0)).
-> 2165     raise ValueError("\n" + ParseException.explain(err, 0)) from None
   2166 self._state_stack = []
   2167 self._in_subscript_or_superscript = False

ValueError: 
$\\theta_{1}$
^
ParseException: Expected end of text, found '$'  (at char 0), (line:1, col:1)
<Figure size 640x480 with 1 Axes>
In [ ]: